11. Warsztaty JS cz.2
Wyzwania:
- dokończysz podstronę rezerwacji,
- rozwiniesz swój projekt o dodatkowe funkcjonalności,
- opublikujesz swoją stronę w internecie,
- podsumujesz i uporządkujesz zdobyte dotychczas umiejętności.
Wstęp
Przez poprzednie trzy moduły pracowaliśmy nad projektem strony pizzerii, przy okazji ucząc się programowania obiektowego i wielu innych zagadnień. Teraz przyszedł czas na nabranie większej wprawy w korzystaniu z nabytej wiedzy. Nasz projekt rozwinie się jeszcze bardziej i to ponownie, głównie Twoimi rękami.
Nie bój się jednak. Pamiętaj, że każde zadanie da się zrobić, nawet jeśli w pierwszych chwilach nie masz pomysłu, jak się za nie zabrać. Warto najpierw zapisać sobie, jaki jest cel zadania, a następnie podzielić ten cel na kilka etapów. Każdy z nich następnie można rozbić na poszczególne kroki, aby w rezultacie uzyskać dość szczegółowy plan realizacji zadania.
Rozpoczynając implementację tego planu, pamiętaj o testowaniu każdego kroku za pomocą console.log i wspomaganiu się debuggerem. Dzięki temu unikniesz błędów, które trudno znaleźć pośród dziesiątek linii kodu napisanego od ostatniego sprawdzenia. Pomoże Ci to również lepiej rozumieć, jak działa Twój kod JS, dzięki czemu szybciej będziesz nabierać wprawy w programowaniu.
A jeśli na czymś utkniesz – zrób sobie przerwę, pogadaj z gumową kaczuszką, albo narysuj na kartce schemat działania swojego skryptu. Każde z tych rozwiązań pomaga spojrzeć na problem z innej perspektywy, co zwykle pomaga w pokonaniu trudności. Oczywiście, do dyspozycji masz też wsparcie Mentora oraz społeczności Kursantów na komunikatorze. Pamiętaj jednak, że w swojej przyszłej pracy możesz nie mieć tak dużego wsparcia, więc zanim poprosisz o pomoc, spróbuj poradzić sobie samodzielnie, a potem poszukać rozwiązań w internecie. Dopiero wtedy – logicznie i po kolei – opisz swój problem i poproś o pomoc Mentora lub innych Kursantów.
Przed Tobą ostatnie zadania w tym projekcie oraz publikacja na serwerze. Powodzenia!
11.1. Dziedziczenie klas
W ostatnim module zaledwie "dotknęliśmy" naszej strony Booking. Udało nam się wygenerować jej widok i uruchomić dwa inputy. Czekają jednak na nas jeszcze dwie znacznie ciekawsze opcje, które również dobrze byłoby połączyć z aplikacją. Tak samo, jak robiliśmy to z polami liczby godzin i osób. Mowa o opcjach wyboru daty i godziny rezerwacji.
Zauważ, że mimo faktu, iż te dwie opcje wyglądają zupełnie inaczej niż pola liczbowe wykorzystane wcześniej, to właściwie potrzebujemy bardzo podobnej logiki, która się nimi zajmie. Też musimy obserwować zmianę ich wartości, dobrze byłoby przechowywać ją w JS i równie warto byłoby informować o każdej zmianie przy pomocy eventu. Tym samym, możemy dojść do wniosku, że warto wykorzystać do ich obsługi tę samą klasę co wcześniej AmountWidget, ale czy na pewno?
AmountWidget jest w tej chwili przystosowany do obsługi inputów liczbowych. Wydaje się więc, że to odpada. Z drugiej strony, czy warto robić nową, tak bardzo podobną klasę? Raczej nie...
Na szczęście istnieje lepsze wyjście. JS pozwala na wykorzystywanie specjalnego mechanizmu, tzw. dziedziczenia klas. Polega on na tym, że jedna klasa bazowa może być wykorzystywana właśnie jako "baza" pod dowolną liczbę klas pochodnych. Tym samym możemy stworzyć klasę bazową, która posiada wszystkie wspólne wykorzystywane w każdej opcji metody, a później na jej podstawie stworzyć klasy pochodne, które tylko lekko rozwiną je o potrzebne w danej sytuacji dodatkowe metody.
Brzmi ciekawie? No to dość czytania, zapraszamy do materiału wideo :)
Za nami sporo pracy, ale zakończonej powodzeniem. Udało nam się wydzielić specjalną klasę bazową, która może i w tej chwili jest jeszcze wykorzystywana tyko przez AmountWidget, ale z powodzeniem można użyć jej też w nowych klasach. Już wcześniej mówiliśmy, że taka "baza" nada się np. dla klas obsługujących wybór godziny i daty, a teraz wprowadzimy ten pomysł w życie.
Zanim jednak do tego przejdziemy zapamiętaj kilka ważnych informacji z wideolekcji:
- Klasa pochodna ma dostęp do wszystkich metod z klasy bazowej.
- Jeśli istnieją dwie metody o takiej samej nazwie zarówno w klasie bazowej, jak i pochodnej, to pierwszeństwo ma metoda w klasie pochodnej i tylko ona jest wywołana. W ten sposób można też łatwo "ignorować" wybrane metody z klasy bazowej. Jeśli np. nie chcemy, aby w naszej klasie pochodnej funkcja
renderValuezBaseWidgetrobiła cokolwiek, to wystarczy, że stworzymy w takiej klasie pustą metodę o takiej samej nazwie. - Klasa pochodna, oparta na klasie bazowej, w swoim konstruktorze zawsze musi wywoływać konstruktor klasy bazowej. Można to osiągnąć poprzez uruchomienie funkcji
super.
Przejdźmy teraz do podstrony Bookingu. Nasze pola wyboru liczby osób i liczby godzin już działają. Teraz zabierzemy się za pola do wyboru daty i godziny.
Pierwszy z nich to na razie pusty input. Za chwilę dodamy do niego plugin flatpickr, który po kliknięciu w input będzie wyświetlał kalendarz, pozwalający na wybór daty.
Wybór godziny będzie odbywał się za pomocą suwaka. Jego skrajna lewa pozycja będzie godziną otwarcia restauracji, a skrajna prawa – godziną zamknięcia. Dodamy plugin rangeSlider, dzięki któremu suwak będzie wyglądał o wiele lepiej, niż domyślny <input type="range">.
Pierwsze pole będzie wyglądać następująco:
A drugie:
Funkcjonalność graficznego wyboru daty czy też mechanizm suwaka do wyboru godziny to pomysły, które są wykorzystywane w branży od lat. Czy warto ciągle pisać taki kod od nowa? Nie lepiej przygotować jakiś kod raz i potem go w razie potrzeby po prostu kopiować? Jak najbardziej!
Na szczęście branża też jest podobnego zdania. Na rynku jest masa darmowych pluginów do łatwego dodawania galerii, datepickera, suwaka itd. Niektóre z nich są napisane lepiej, niektóre gorzej, ale nie sposób odmówić im tego, że bardzo często znacznie przyspieszają nam pracę.
Czym są te owe "pluginy"? Tak naprawdę mamy tutaj na myśli po prostu gotowe pliki .js z przygotowanymi funkcjami czy też klasami. Spójrz np. na naszą klasę AmountWidget. Właściwie nie jest ona w żaden sposób specyficzna dla naszej aplikacji. Tak naprawdę moglibyśmy wrzucić taki plik na GitHuba wraz z dokumentacją, jak się tej klasy używa i tyle... Też można by ją traktować jako "plugin". W końcu każdy mógłby taką klasę pobrać i wykorzystać bez problemu w swoim własnym projekcie!
Czasami plugin/biblioteka dostarcza tylko plik .js np. z gotową klasą lub funkcją. Innym razem autor skryptu prosi też o dołączenie do projektu jakiegoś arkusza CSS. Ma to sens. Jeśli np. skrypt powinien zajmować się renderowaniem na stronie karuzeli, to siłą rzeczy przydałby się arkusz, który odpowiednio by ją ostylował. Plik .js mógłby się zająć logiką, a więc np. udostępnianiem możliwości ruchu karuzeli, ale wygląd to już w końcu nie jego broszka.
Mówiąc plugin mamy na myśli po prostu gotowy plik lub pliki JS (czasem również CSS), które są na tyle uniwersalnie napisane, że można wykorzystać je bez problemu na wielu witrynach.
Na koniec może pojawić się jeszcze jednio pytanie – czy pluginy podążają za jakimś schematem? Czy zawsze są zbudowane podobnie? Otóż nie. Tak naprawdę różne pluginy są tworzone przez różne osoby. Każdy autor sam decyduje, jak dany skrypt napisze. Sam ustala, czy będzie funkcją, czy klasą. Sam decyduje również, jak się go uruchamia. Owszem, bardzo często trafiamy na podobne pomysły, np. bardzo popularne jest uruchamianie pluginu poprzez tworzenie instancji jego klasy, ale... nie jest to "obowiązkowe" i mogą się trafić pluginy zbudowane inaczej. Dlatego też bardzo ważne jest to, żeby plugin mógł pochwalić się dobrą dokumentacją. Tak, żebyśmy nie musieli się domyślać, jak można go uruchomić. Tu przy okazji pojawia się istotna rada. Jeśli zastanawiasz się nad wyborem pluginu, to jakość jego dokumentacji powinna być jednym z głównych kryteriów wyboru. Oszczędzi Ci to bólu głowy.
No dobra, czas brać się do pracy. Zapewne po tym, co zrobiliśmy z AmountWidget, masz mieszane uczucia. Z jednej strony łatwo zrozumieć, po co ten podział na klasę bazową i pochodną. Po krótkiej analizie da się też rozpracować co tam się właściwie dzieje. Co innego jednak to rozumieć, a napisać nowy widget i to jeszcze wykorzystujący zewnętrzny plugin! No cóż, masz rację. Może nie byłoby to bardzo trudne, ale uwierz nam, dość czasochłonne.
Dlatego też nie będziemy Cię męczyć i obie klasy (widgety) dostarczymy w gotowej formie. Twoim zadaniem będzie tylko dodanie ich do projektu, przeanalizowanie i wykorzystanie.
Zacznij więc od pobrania tych klas i wklejenia ich do folderu components.
Przygotowujemy dostęp do pluginów
Zanim jednak przejdziemy do ich analizy, musimy jeszcze pobrać odpowiednie pluginy. Oba widgety opierają się na zewnętrznych skryptach, musimy więc dostarczyć je do naszej aplikacji. Nie będzie to jednak trudne. Dokumentacje pluginów zawsze instruują, jakie pliki musimy pobrać, czy też dołączyć, do aplikacji.
Zaczniemy od flatpickr, który jest wykorzystywany przez klasę DatePicker do pokazywania okienka wyboru daty.
Znajdźmy więc w dokumentację sekcję "Getting started" – instrukcją pluginu. Jak widzisz, nie musimy się niczego domyślać. Dokumentacja bardzo dokładnie informuje nas, co musimy podłączyć do aplikacji: skrypt JS pluginu oraz dodatkowo arkusz CSS. Nic w tym nadzwyczajnego. Mówiliśmy już wcześniej, że niektóre pluginy faktycznie potrzebują też własnych stylów i właśnie tak jest też w tym przypadku.
Podłącz więc oba pliki do naszego pliku index.html. Pamiętaj przy tym, że style umieszczamy w <head> przed naszymi stylami (css/style.css), a skrypty na końcu <body> przed odwołaniem do app.js.
Czas na drugi plugin, wykorzystywany przez HourPicker. Tę paczkę musimy już pobrać. Autor nie udostępnia bowiem linków do CDN-a, jak było to w przypadku flatpickr. Możesz to zrobić [tutaj] (https://github.com/Stryzhevskyi/rangeSlider/releases). Pobierz najnowsze wydanie, a pliki range-slider.min.css i range-slider.min.js z tej paczki umieść w katalogu src/vendor. Następnie dodaj odwołania do tych plików w pliku index.html, podobnie jak wcześniej dla pluginu flatpickr.
Zauważ, że ta różnica tylko potwierdza to, o czym mówiliśmy wcześniej. Pluginy są tworzone przez różne osoby i każdy autor sam decyduje, jak jego plugin działa pod maską, jak się go używa, no i gdzie go udostępni. Raz może oferować nam CDN, a innym razem wymagać pobrania plików.
Analiza nowych klas
Możemy teraz przejść do analizy naszych klas. DatePicker omówimy sobie krótko razem. Analizę HourPicker zostawimy już Tobie.
DatePicker – konstruktor
class DatePicker extends BaseWidget {
constructor(wrapper) {
super(wrapper, utils.dateToStr(new Date()))
...
}
}
Na samym początku zajrzyjmy do konstruktora. Po pierwsze, zauważ, że nie ustala on na sztywno referencji do elementu, na którym będzie działać. Zamiast tego oczekuje, że taka referencja będzie przekazana w formie argumentu konstruktora. Jest to zamysł, który wykorzystywaliśmy również w AmountWidget. Chcemy, aby nasz DatePicker mógł być używany wiele razy, a dzięki takiemu mechanizmowi, za każdym razem będzie on mógł otrzymywać referencję do innego elementu, innego inputu. Na razie myślimy bowiem o tym, aby wykorzystać ten widget konkretnie w bookingu, ale... przecież nic nie stoi na przeszkodzie, żeby użyć go w przyszłości w innym miejscu. W końcu datePicker to w zamyśle dość uniwersalny widget.
Równie ciekawa jest wartość startowa, którą przekazujemy do konstruktora klasy bazowej. Jak już wiesz, klasa pochodna w konstruktorze musi włączyć konstruktor klasy bazowej (super). Klasa BaseWidget oczekuje w swoim konstruktorze na dwa argumenty – wrapperElement i initialValue.
Zrozumiałe więc, że jako pierwszy przekazujemy właśnie referencję do elementu otrzymaną w argumencie konstruktora (wrapper). A co z drugim? Co ustawiamy jako domyślną wartość? Po prostu aktualną datę. To chyba sensowna opcja na wartość startową, prawda?
Możesz dziwić się, w jakim celu korzystamy z funkcji utils.dateToStr, zamiast od razu przekazać tylko new Date(), czyli aktualną datę. No cóż. Odpowiedź jest prosta, ustalamy sobie wartość w formacie, który jest dla nas wygodniejszy. Musisz wiedzieć, że data to w JS specjalny typ danych, który nie jest najprzyjemniejszy w obsłudze. W naszej sytuacji znacznie łatwiej będzie pracować na tekście.
Kilka słów o datach
Jeśli chodzi o daty, potrzebne Ci są jedynie trzy informacje – po pierwsze, new Date() tworzy obiekt daty, którego wartość to "teraz", czyli data i godzina w momencie wykonania tego kodu JS.
Po drugie, aby uzyskać datę przesuniętą o ileś dni, możesz używać przygotowanej przez nas funkcji utils.addDays – przyjmuje ona dwa argumenty: datę, do której ma dodać ilość dni, oraz ilość dni, która ma być dodana.
Wreszcie po trzecie, utils.dateToStr przekształca obiekt daty na tekst w wygodniejszym formacie rok-miesiąc-dzień, czyli np. '2019-12-31'.
thisWidget.dom.input = thisWidget.dom.wrapper.querySelector(select.widgets.datePicker.input);
thisWidget.initPlugin();
To, co dalej jest raczej oczywiste. Przygotowujemy referencję do pola tekstowego, na którym będzie działać nasz plugin flatpickr i uruchamiamy metodę initPlugin. Jej zawartością zajmiemy się za moment.
DatePicker – initPlugin
Czas na analizę najciekawszej metody – initPlugin, która jak już wiesz, jest uruchamiana na samym początku działania klasy.
initPlugin(){
const thisWidget = this;
thisWidget.minDate = new Date();
thisWidget.maxDate = utils.addDays(thisWidget.minDate, settings.datePicker.maxDaysInFuture);
// eslint-disable-next-line no-undef
flatpickr(thisWidget.dom.input, {
defaultDate: thisWidget.minDate,
minDate: thisWidget.minDate,
maxDate: thisWidget.maxDate,
locale: {
firstDayOfWeek: 1
},
disable: [
function(date) {
return (date.getDay() === 1);
}
],
onChange: function(selectedDates, dateStr) {
thisWidget.value = dateStr;
},
});
}
Korzystanie z flatpickr jest dość proste. Tak naprawdę to tylko zwykła funkcja, która przyjmuje dwa argumenty.
flatpickr(element, options);
Pierwszy przyjmuje referencję do inputu, na którym plugin ma się uruchomić. Druga informuje, z jakimi opcjami ma to zrobić. Włączenie tej funkcji wystarczy więc, aby zwykły input stał się w "magiczny" sposób datePickerem.
Drugim argumentem jest obiekt zawierający opcje pluginu. Najczęściej autorzy pluginów nie wymagają ustawiania wielu opcji, większość ma swoje wartości domyślne. Bardzo często jednak mamy łatwą możliwość ich zmiany i tak jest również w naszej sytuacji. Dzięki temu możemy w łatwy sposób trochę bardziej spersonalizować działanie pluginu.
Spójrz, jakie opcje ustawiliśmy.
defaultDate: thisWidget.minDate,
Ustawia domyślną datę na wartość thisWidget.minDate (czyli tak naprawdę aktualną datę).
minDate: thisWidget.minDate,
Ustala minimalną datę. Również jako wartość wybieramy tu aktualną datę. Dzięki temu nie można wybrać terminu, który już minął. Ma to sens. Nie możemy bowiem pozwalać na rezerwację w terminach, które już za nami.
maxDate: thisWidget.maxDate,
Ustala maksymalną datę do wybrania. Zauważ, że tak naprawdę jest to aktualna data + liczba dni ustawiona w settingsach. Oznacza to, że jeśli wartość settings.datePicker.maxDaysInFuture to np. 14, to możemy wybierać terminy z zakresu od dzisiaj do 14 dni w przód. Nie dalej.
locale: {
firstDayOfWeek: 1
},
Ustala, aby pierwszym dniem tygodnia zawsze był poniedziałek. Chodzi tutaj po prostu o wygląd planszy do wyboru dni, którą będzie uruchamiać flatpickr. Chcemy, aby poniedziałek był traktowany jako pierwszy dzień.
Myślimy o układzie:
Pon Wto Śr Czw Pią Sob Nie
Skąd taka potrzeba? No cóż, plugin nam niczego nie narzuca. Inna firma może pracować np. od wtorku do piątku i wolałaby, aby kalendarz pokazywał się w takim układzie:
Wto Śr Czw Pią Sob Nie Pon
To kwestia wyboru. U nas pokazujemy kalendarz od poniedziałku.
disable: [
function(date) {
return (date.getDay() === 1);
}
],
Nasza restauracja jest nieczynna w poniedziałki. Ta opcja zadba o blokowanie tego dnia przy wyborze.
onChange: function(selectedDates, dateStr) {
thisWidget.value = dateStr;
},
Pozostała nam tylko funkcja callback (onChange). Będzie ona uruchamiana, gdy plugin wykryje zmianę terminu. Zauważ, że wynikiem działania tej funkcji będzie po prostu zaktualizowanie thisWidget.value. I tak naprawdę to jest dla nas najważniejsze.
Czy musimy zastanawiać się, jak flatpickr działa pod maską? Tak naprawdę nie. Oczywiście możemy zgadywać, że zapewne na samym początku dodaje ona nasłuchiwacz na thisWidget.dom.input. Tak, aby w momencie wykrycia kliknięcia, pokazywać kalendarz. Możemy obstawiać, że każdy dzień w kalendarzu również ma nasłuchiwacz i kliknięcie na niego, faktycznie uruchomi np. funkcję callback (onChange), ale... czy tak naprawdę ma to dla nas znaczenie? Nie.
Funkcja flatpickr może być prosta, może być trudna, ale nas to nie interesuje. To, co jest dla nas ważne, to tylko informacja jak można ją uruchomić, ew. z jakimi opcjami i jak można wykryć zmianę oraz dostać się do wybranej wartości. Te wszystkie informacje są już dostępne w dokumentacji.
Tak dużo opcji...
Liczba opcji, które wykorzystaliśmy, może wywołać lekki ból głowy. Zwłaszcza niektóre były mało intuicyjne.
Np.
disable: [
function(date) {
return (date.getDay() === 1);
}
],
Oj ciężko byłoby wpaść na to, że odpowiada ona za blokowanie poniedziałku.
Pamiętaj jednak, że nikt nie oczekuje tego, że masz się tego domyślać. My też wcale nie "strzelaliśmy". Wszystkie opcje są po prostu opisane w dokumentacji. Wystarczy ją prześledzić, zobaczyć podane przykłady i wykorzystać, jeśli mamy na to ochotę. To tak samo, jak z Bootstapem. Nikt nie oczekuje, że masz pamiętać wszystkie nazwy klas. Jeśli ich potrzebujesz, wystarczy sprawdzić dokumentację Bootstrapa i wszystko mamy na tacy.
DatePicker – pozostałe metody
Pozostały nam już tylko trzy proste metody.
parseValue(value){
return value;
}
isValid(){
return true;
}
renderValue(){
}
Po co nam one? Po to, aby "unieszkodliwić" oryginalne metody z BaseWidget.
Pamiętaj, że DatePicker jest klasą pochodną od BaseWidget oraz, że zmiana wartości thisWidget.value uruchomi z automatu setter, który jest zawarty w klasie bazowej.
Przypomnijmy, jak on wygląda.
set value(value){
const thisWidget = this;
const newValue = thisWidget.parseValue(value);
if (newValue != thisWidget.correctValue && thisWidget.isValid(value)) {
thisWidget.correctValue = newValue;
thisWidget.announce();
}
thisWidget.renderValue();
}
Zobacz, że m.in. uruchamia on metody parseValue, isValid i renerValue. Pierwsza ma starać się odpowiednio parsować wartość, druga sprawdza jej poprawność, a trzecia renderuje wartość w HTML-u.
Teraz zobacz, jak te metody w klasie BaseWidget wyglądają.
parseValue(value){
return parseInt(value);
}
isValid(value){
return !isNaN(value);
}
renderValue(){
const thisWidget = this;
thisWidget.dom.wrapper.innerHTML = thisWidget.value;
}
Jeśli JS skorzysta z nich podczas ustawiania wartości, która jest stringiem z datą (np. 02-02-2020), szybko natrafi na problem. Już parseValue zwróci nam błąd przy próbie konwersji na integer. isValid tylko to potwierdzi, stwierdzając, że nasza wartość jest niepoprawna. Tak samo w przypadku tego widgetu sensu nie ma funkcja renderValue.
Jak widzisz, te funkcje były w porządku dla AmountWidgetu, ale do naszego nowego widgetu już nie pasują. Nie możemy ich jednak usunąć, gdyż AmountWidget nadal z nich korzysta. Dlatego też robimy coś innego. Wykorzystujemy swoją wiedzę na temat klas i fakt, że jeśli w klasie pochodnej jest taka metoda o takiej samej nazwie jak w klasie bazowej, to zawsze ważniejsza będzie ta, w klasie pochodnej.
Ustawiając wiec nasze trzy własne metody parseValue, isValid i renderValue, po prostu zapewniamy sobie to, że setter w BaseWidget będzie uruchamiał w przypadku DatePicker właśnie je, a nie oryginały. Tym samym niczego nam nie popsuje, bo te nowe wersje metod są już dla nas bezpieczne.
Klasa HourPicker
Drugą klasę przeanalizuj już bez naszej pomocy. Zapewne zauważysz, że tak naprawdę będzie ona bardzo podobna. Różnicą jest tylko inny plugin, z którego korzystaliśmy. Jego autorem był ktoś inny niż w przypadku flatpickr, więc i jego użycie trochę się różni.
Zadanie: użycie widgetów DatePicker i HourPicker
Twoim zadaniem jest skorzystanie z obu widgetów w klasie Booking. Pamiętaj, że oba działają bardzo podobnie jak AmountWidget. Przy tworzeniu nowej instancji wystarczy więc podać referencję do elementu, dany widget ma się "uruchomić", np. jakiś input. Dokładnie tak samo, jak robiliśmy to w przypadku AmountWidget.
Tym samym musisz zrobić tylko dwie rzeczy.
- Po pierwsze, w obiekcie
thisWidget.domw metodzierenderprzygotuj referencje do obu inputów. Ich selektory toselect.widgets.datePicker.wrapperiselect.widgets.hourPicker.wrapper. - Po drugie, w metodzie
initWidgetszadbaj o to, aby uruchomić na obu elementach odpowiednie widgety.
Podsumowanie zadania
Jeśli wszystko poszło dobrze, mamy już działający wybór daty i godziny. Oba elementy są bardzo przyjazne dla użytkownika i pozwalają na intuicyjną obsługę formularza.
Pod nimi widzimy jednak mapę restauracji, która na razie nie ma żadnej funkcjonalności. Już za chwilę to zmienimy!
11.2. Pobieranie przefiltrowanych danych z API
Mapa restauracji ma służyć do wyboru dostępnego stolika, uwzględniając datę i godzinę. Aby to było możliwe, nasz skrypt musi najpierw wiedzieć, kiedy poszczególne stoliki są zajęte. Właśnie tym zajmiemy się w tej części warsztatów.
Promise'y to mechanizm, z którym początkujący mają często najwięcej problemów. Jeśli nie czujesz się jeszcze pewnie w tym temacie, to polecamy ten artykuł na portalu MDN. Stara się on zawrzeć wszystko, co musisz wiedzieć na temat promise'ów.
11.3. Agregacja danych i aktualizacja DOM
Pobraliśmy już przefiltrowane dane z API – teraz czas je wykorzystać!
To już ostatnia część warsztatów, którą realizujemy wspólnie. Dalszy rozwój tego projektu będzie Twoim zadaniem – ale nie martw się, zostało już niewiele do zrobienia!
Zadanie: wysyłanie rezerwacji do API
Na tym etapie formularz jest już prawie gotowy, ale nie do końca. Możemy wybierać liczbę gości, liczbę godzin, termin rezerwacji, godzinę rezerwacji, a do tego na bieżąco widzimy, które stoliki są wolne. To jednak wciąż za mało, aby uznać naszą podstronę rezerwacji za gotową.
Po pierwsze, widzimy jakie stoliki są wolne, a jakie zajęte, ale nie możemy wskazać, który chcielibyśmy wybrać jako klient.
Po drugie, kiedy wybierzemy już wszystkie opcje, chcielibyśmy wysłać naszą rezerwację do serwera, ale nasz button "Book a table" po kliknięciu jeszcze niczego takiego nie robi...
Wykonanie obu tych funkcjonalności będzie teraz Twoim zadaniem.
Wybór stolika
Warto zacząć od możliwości zaznaczenia dostępnego stolika za pomocą kliknięcia.
Jak to w ogóle powinno działać? Klient w każdej chwili powinien być w stanie kliknąć na dowolny wolny stolik. Kliknięcie na taki stolik powinno zmieniać jego wygląd, np. nadawać nową klasę, która doda mu dodatkowy border albo inny kolor. Powinno również zapisywać informacje o wybranym stoliku w instancji Bookingu, np. jako właściwość. Po co? Po to, aby później, przy próbie wysyłki rezerwacji do serwera, mieć dostęp do informacji, jaki stolik został wybrany przez klienta.
To dokładnie ten sam pomysł, jak w przypadku AmountWidget-ów. Zawsze, gdy tworzyliśmy w jakiejś klasie ich instancje, od razu przypisywaliśmy referencje do nich jako właściwości. Po co? W tym samym celu. Żeby w przyszłości mieć łatwy dostęp do ich wartości w pozostałych metodach.
Podsumowując, na pewno kliknięcie na wolny stolik powinno zmienić jego wygląd oraz zapisać jego numerek do właściwości w instancji Booking.
Dodatkowo musimy założyć, że:
- klient nie może wybrać stolika, który jest już zajęty,
- wybór stolika powinien być resetowany przy zmianie godziny, daty, liczby gości oraz liczby godzin,
- kliknięcie na zaznaczony stolik po raz kolejny, również powinno wyzerować wybór i przywrócić podstawowy wygląd stolika,
- jeśli klient zaznaczył już stolik, a po tym klika jednak na inny, to w takiej sytuacji trzeba zaktualizować informację o wybranym stoliku oraz zabrać klasę "wyboru" ze starego stolika.
- nie chcemy nasłuchiwać na stoliki pojedynczo, zastosuj event delegation.
Całość powinna na końcu działać następująco:
Uwaga! Staraj się trzymać praktyk, które wykorzystywaliśmy przez cały projekt, czyli np. do przechowywania selektorów i klas wykorzystuj obiekty select i classNames, czy też przypisuj ew. referencje do elementów HTML w obiekcie thisBooking.dom.
Wskazówki
- Tak naprawdę wykonywaliśmy już kiedyś podobne ćwiczenie. Pamiętasz naszą aplikację z książkami? Stworzyliśmy tam mechanizm wyboru ulubionych książek, który działa naprawdę bardzo podobnie! Z tym że tam było nawet trudniej, bo pozwalaliśmy na wybieranie więcej niż jednej książki. Tutaj zawsze będziemy mieć informację tylko o jednym stoliku.
- Specjalną klasę, którą można by nadać wybranemu stolikowi, musisz dopiero utworzyć. Nie ma jej jeszcze w arkuszu stylów.
- Jak sprawdzić, czy stolik jest wolny? Pamiętaj, że każdy zajęty stolik otrzymuje od
updateDOMklasębooked. Aby sprawdzić, czy stolik jest wolny, nie trzeba więc sprawdzać bezpośrednio obiektuthisBooking.booked. Wystarczy, że sprawdzimy, czy stolik ma klasębooked. Jeśli ma, to na pewno jest zajęty! Jeśli nie, to na pewno jest wolny! - Jak zadbać o "zerowanie" wyboru stolika przy zmianie godziny czy daty? Czy trzeba dodawać jakieś nasłuchiwacze na inputy, które po wykryciu zmiany, wyzerują właściwość i odbiorą wybranemu stolikowi klasę "wyboru"? Nie... Można to zrobić znacznie prościej. Odpowiemy jednak enigmatycznie – sprawdź, kiedy uruchamiana jest funkcja
updateDOM;)
Spróbuj wykonać to zadanie bez naszej pomocy. Wspomagaj się również kodem, który napisaliśmy w aplikacji książek. Naprawdę dużo Ci on podpowie. Jeśli przez dłuższy czas nie uda Ci się niczego wymyślić, to poniżej przedstawiamy dokładniejsze kroki, od których możesz zacząć.
Pokaż wskazówkę Ukryj wskazówkę
-
Zacznij od utworzenia w CSS nowej klasy. Nazwij ją np.
selectedi zadbaj, aby była podobna do klasybooked, ale np. dodawała jeszcze jakiś border albo inny background. Chodzi po prostu o to, aby klient łatwo mógł odróżnić stoliki faktycznie zarezerwowane od tego, który sam zaznaczył. -
Następnie przygotuj w konstruktorze właściwość, która w założeniu będzie przechowywać informacje o wybranym stoliku.
-
Przygotuj w metodzie
renderdostęp do diva ze stolika. Co ważne, całego diva. -
Dodaj do
initWidgetsnowy nasłuchiwacz, który przy wykryciu kliknięcia w diva ze stolikami włączy nową metodę. Np.initTables. -
Metoda ta powinna sprawdzać, czy kliknięto faktycznie na stolik. Jeśli tak, to powinna sprawdzać, czy stolik jest wolny. Jeśli nie, to może pokazywać np. alert z komunikatem o zajętości stolika. Jeśli jednak jest wolny, to należy przypisać numer tego stolika do właściwości, którą wcześniej przygotowaliśmy w konstruktorze, tak aby cała aplikacja miała dostęp do tej informacji. Należy również dodać do tego stolika klasę
selected. -
Pamiętaj przy tym, że zanim dodasz do wybranego stolika klasę
selected, to należy sprawdzić, czy czasem inny stolik już takiej nie ma. I jeśli ma, to mu ją zabrać. Samej właściwości nie musimy jednak wcześniej zerować, bo przecież i tak za moment nadpiszemy ją numerkiem nowego stolika. Musimy więc zadbać tylko o zabranie klasy staremu stolikowi, samą właściwością się nie martw.
To już naprawdę dużo. Od tej chwili kliknięcie na stolik zajęty, powinno pokazywać od razu alert z informacją o zajętości. Kliknięcie na wolny stolik powinno powodować zmianę jego wyglądu i, co ważniejsze, przypisywać do jakiejś właściwości w Booking informacje o numerze tego wybranego stolika. Co więcej, jeśli w momencie wyboru nowego stoliku, inny był już wybrany wcześniej, to ten wcześniejszy powinien wrócić wyglądem do normalności, czyli stracić klasę selected.
To, co Ci pozostało, to już tylko poniższe funkcjonalności:
- wybór stolika powinien być resetowany przy zmianie godziny, daty, liczby gości oraz liczby godzin,
- kliknięcie na zaznaczony już stolik po raz kolejny, powinno również resetować wybór zapisany w JS oraz zabierać klasę "wyboru".
Wysyłka danych na serwer
Druga część zadania będzie wyraźnie łatwiejsza. Musimy bowiem przygotować funkcję, która po prostu wyślę pod odpowiedni adres na serwerze informacje o rezerwacji. Tyle. Tak naprawdę ta metoda będzie więc prawie identyczna jak sendOrder, które napisaliśmy w klasie Cart. Inny będzie jedynie link, no i zawartość obiektu payload.
Twoim zadaniem będzie więc:
- Zadbanie o to, aby próba wysyłki formularza, kończyła się włączeniem funkcji
sendBooking. - Przygotowanie funkcji
sendBooking, która wyśle pod adreslocalhost:3131/bookingobiekt o następującej strukturze:
{
"date": data wybrana w datePickerze
"hour": godzina wybrana w hourPickerze (w formacie HH:ss)
"table": numer wybranego stolika (lub null jeśli nic nie wybrano)
"duration": liczba godzin wybrana przez klienta
"ppl": liczba osób wybrana przez klienta
"starters": [],
"phone": numer telefonu z formularza,
"address": adres z formularza
}
Przy czym ppl, duration i table powinny być liczbami. Nie mogą być tekstem.
Nie opisaliśmy tutaj tylko jednej właściwości – starters. W Cart.sendOrder mieliśmy jedną specjalną właściwość, która była tablicą, uzupełnianą dopiero pod obiektem payload. Tutaj jest podobnie. starters powinno być tablicą, która zawiera wartości zaznaczonych checkboxów w sekcji "Starters".
Jeśli zaznaczono tylko "water", to starters powinno być tablicą ['water']. Jeśli zaznaczono Bread, to starters powinno być tablicą ['water', 'bread']. Jeśli nie zaznaczono nic, to powinno być pustą tablicą.
Poradzisz z tym sobie, prawda? Przypomnij sobie ponownie projekt aplikacji z książkami. Robiliśmy tam coś podobnego w funkcjonalności z filtrami.
Użycie makeBooked...
O mało nie zapomnieliśmy! Pamiętaj, że request, który wykonamy, nie odświeża naszej strony. Z jednej strony, to oczywiście plus, ale z drugiej, w naszym przypadku, ma pewną wadę. Oznacza to bowiem, że w thisBooking.booked nie będzie zapisana informacja o naszej rezerwacji. Tak naprawdę, dopiero odświeżenie strony i pobranie danych z serwera, spowodowałoby ponowne przygotowanie thisBooking.booked, w którym nowa rezerwacja już by się znalazła.
Co możemy z tym fantem zrobić? Tak naprawdę pobieranie danych to tylko jeden etap przygotowania thisBooking.booked. Zauważ, że gdy są one już pobrane, to rezerwacje są dodawane jedna po jednej, za pomocą funkcji makeBooked. Wystarczy więc, że za pomocą tej samej funkcji, w przypadku sukcesu naszego requestu do serwera, będziemy dodawać do thisBooking.booked również naszą nową rezerwację!
Powodzenia!
11.4. Dodajemy stronę główną
Nasza witryna pizzerii jest prawie gotowa. Już wcześniej udało nam się wykonać podstronę z produktami oraz funkcjonalność koszyka. W poprzednim submodule dokończyliśmy również temat podstrony rezerwacji. Pozostała nam do zrobienia jeszcze jedna podstrona – powitalna. Jej wykonanie będzie już w pełni Twoim zadaniem.
Zadanie: nowa podstrona
Efekt, który chcemy osiągnąć, powinien być następujący:
Do napisania będzie przede wszystkim sporo HTML-a, konieczne będą również nowe style CSS, nie obędzie się jednak również bez odrobiny JS-a. Do zaimplementowania są następujące funkcjonalności:
- cytaty z opiniami o pizzerii powinny znajdować się w karuzeli, która będzie automatycznie i w nieskończoność przewijać się do kolejnego cytatu co 3 sekundy,
- kliknięcie w box zachęcający do zamówienia online powinno otwierać podstronę Order (razem z zaznaczeniem odpowiedniego linku w nawigacji jako aktywnego),
- analogicznie, kliknięcie w box informujący o możliwości rezerwacji stolika powinno przenosić użytkownika na podstronę Booking.
Oprócz tego zwróć uwagę na następujące efekty (do zakodowania w CSS):
- gdy najedzie się na któryś z mniejszych boksów w pierwszej sekcji, napisy na nim animują się,
- w galerii po najechaniu na zdjęcie pojawiają się ikonki pochodzące z Font Awesome,
- ikonki powinny być domyślnie białe, a na hover mieć kolor przewodni Twojej strony,
- oprócz ikonek, po najechaniu na zdjęcie pojawia się overlay.
Pamiętaj o efektach przejścia, by wszystkie animacje były płynne.
Zdjęcia, które należy użyć w projekcie, wraz z plikiem .psd designu, możesz pobrać poniżej.
Nowy link w menu
Oprócz dodania samej podstrony, nie zapomnij również o dodaniu nowego linku w pasku nawigacji.
Zadbaj również o to, aby to właśnie ta podstrona była zawsze otwierana jako pierwsza.
Karuzela
Nie musisz pisać skryptu karuzeli od zera. Możesz wykorzystać wybrany dostępny w sieci plugin. Z naszej strony polecamy dwa rozwiązania – Flickity oraz TinySlider. Zwłaszcza to pierwsze powinno być stosunkowo łatwe do zaimplementowania.
Kolejna podstrona, ten sam projekt
Jak zacząć? Zauważ, że już raz do naszego projektu dodawaliśmy nową podstronę. Była nią podstrona Booking. Warto więc wrócić w materiale do momentu, w którym zaczęliśmy dodawanie tamtej podstrony i się na tym wzorować.
Pokaż wskazówkę Ukryj wskazówkę
Tak naprawdę, musisz zacząć tak samo. Od stworzenia szablonu w HTML-u, nowej sekcji w divie #pages i klasy (np. Home). Klasy, która z pewnością będzie (tak samo, jak Booking) posiadać metodę render, renderującą na podstawie tego przygotowanego szablonu widok dla naszej nowej sekcji w #pages. Może ona również od razu przygotować referencje do istotnych elementów DOM na tej podstronie, np. diva ze slajdami karuzeli. Na pewno w klasie Home przyda się również metoda initWidgets, która tę karuzelę z pomocą jakiegoś pluginu uruchomi.
Dla ambitnych
Jeśli chcesz, możesz przygotować dla karuzeli dodatkowo specjalną klasę, która ułatwi jej tworzenie przy pomocy pluginu. Tak np. robiliśmy to przypadku HourPickera. Chociaż oczywiście akurat ta klasa nie powinna opierać się na BaseWidget. Działanie karuzeli za bardzo różni się od tego, co tamta klasa oferowała. Nie ma to więc zbyt dużego sensu. Klasa dla karuzeli powinna być prostą klasą, niebazującą na żadnej innej. Tak naprawdę wystarczą w niej dwie metody render i initPlugin.
Pokaż wskazówkę Ukryj wskazówkę
Taka klasa mogłaby np. wyglądać tak:
class Carousel {
constructor(element) {
const thisCarousel = this;
this.render(element);
this.initPlugin();
}
render(element) {
// save element ref to this obj
}
initPlugin() {
// use plugin to create carousel on thisCarousel.element
}
}
Wtedy w klasie Home moglibyśmy taki widget naprawdę łatwo uruchomić, prawie identycznie jak np. AmountWidget, tworząc po prostu nową instancję i przekazując do niej referencję do diva, która poinformuje klasę o elemencie, na którym trzeba ją uruchomić.
11.5. Publikujemy stronę w internecie
Ostatnim etapem jest jego publikacja projektu w internecie. Dzięki temu będziesz mieć możliwość pokazania efektów swojej pracy znajomym czy innym Kursantom. Po ukończeniu kursu ta umiejętność przyda Ci się, aby opublikować zrealizowane projekty, w celu zaprezentowania swoich umiejętności rekruterom.
Czy publikować swój projekt?
Wielu Kursantów na tym etapie ma obawy dotyczące publikacji swojego projektu w internecie. Może to wynikać z obawy przed pokazaniem "całemu światu" swojego projektu. Pamiętaj jednak, że aby zobaczyć Twój projekt, trzeba znać link do niego – a nawet wtedy nikt nie będzie wiedział, że to właśnie Twoje dzieło.
Jednocześnie, umiejętność publikacji swoich projektów będzie dla Ciebie bardzo ważna na etapie poszukiwania pracy. Dlatego warto już teraz nauczyć się, jak to zrobić.
Co będzie potrzebne?
Do opublikowania projektu w internecie będzie nam potrzebny hosting, czyli miejsce na serwerze, w którym możemy umieścić pliki. Ponadto będziemy potrzebowali aplikacji serwera HTTP, która będzie odpowiadać na zapytania przeglądarki i w odpowiedzi przekazywać jej zawartość plików. Wreszcie, potrzebujemy też domeny, pod którą będzie dostępny nasz opublikowany projekt.
To wszystko zapewni nam bezpłatny hosting na platformie Heroku. Co więcej, publikacja na Heroku będzie relatywnie prosta, ponieważ odbywa się za pomocą Gita!
Czytając opis aplikacji serwera HTTP, mogło Cię zastanowić, dlaczego potrzebujemy jakiegoś rozwiązania, skoro do tej pory mogliśmy bez problemu wyświetlać projekt w przeglądarce. Otóż mogliśmy właśnie dlatego, że korzystamy z serwera HTTP, a nawet dwóch! Jednym z nich jest BrowserSync, a drugim – json-server. Obie te aplikacje są uruchamiane przez taski, które mamy zdefiniowane w package.json.
Pierwszej z nich używamy, podobnie jak we wcześniejszych modułach, do wyświetlania strony. Ma ona dodatkową zaletę w postaci odświeżania strony przy zamianach w plikach projektu. Na opublikowanej wersji projektu nie będzie nam to jednak potrzebne. Będziemy za to potrzebować naszego API obsługiwanego przez json-server. Tak się składa, że ma on też funkcję serwowania plików statycznych, czyli przyjmowania zapytań przeglądarki i przekazywania jej zawartości plików.
Przygotowanie projektu do publikacji
Aby nasz projekt poprawnie działał na Heroku, potrzebujemy wprowadzić w nim kilka drobnych zmian. Przede wszystkim, nasze API nie będzie działać na porcie 3131, tylko na porcie 80 – domyślnym dla połączeń HTTP. Dlatego musimy zmienić w pliku settings.js adres, pod którym będzie dostępne API. Znajdź w tym pliku następującą linię kodu, znajdującą się w obiekcie settings:
url: '//localhost:3131',
Zmienimy ją w taki sposób, aby jej wartością była domena, pod którą oglądamy projekt. Tylko jeśli tą domeną będzie localhost, dodamy do adresu informację o porcie :3131.
url: '//' + window.location.hostname + (window.location.hostname=='localhost' ? ':3131' : ''),
To wszystko, co potrzebowaliśmy zmienić w tym pliku. Po tej zmianie aplikacja powinna dalej poprawnie działać na Twoim komputerze, ale teraz będzie też mogła być uruchomiona na Heroku.
Drugi etap przygotowań związany jest z tym, że nie możemy uruchamiać naszego serwera za pomocą taska w package.json. Wykorzystamy nieznacznie dostosowany przykład z dokumentacji json-server. W tym celu w głównym katalogu projektu (tam, gdzie znajduje się package.json) stwórz plik server.js i wstaw do niego następujący kod:
/* global require, process */
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router('dist/db/app.json');
const middlewares = jsonServer.defaults({
static: 'dist',
noCors: true
});
const port = process.env.PORT || 3131;
server.use(middlewares);
server.use(router);
server.listen(port);
Jest to kod, który nie będzie uruchamiany w przeglądarce, tylko na serwerze. Z interesujących rzeczy w tym kodzie możesz zauważyć ścieżkę do pliku app.json, katalog dist ze zbudowanymi plikami projektu, czy port 3131 – używany, tylko jeśli nie istnieje zdefiniowana przez serwer właściwość process.env.PORT, zawierająca z góry narzucony numer portu.
Aby serwer wiedział, że ma uruchomić ten plik, musimy dodać jeszcze jeden plik konfiguracyjny o nazwie Procfile (bez rozszerzenia), również w głównym katalogu projektu. Jego zawartością będzie jedna linia:
web: node server.js
To już wszystkie przygotowania – jesteśmy gotowi do publikacji!
Stworzenie aplikacji na Heroku
Pierwszym krokiem jest założenie konta na Heroku. Nie martw się, konto jest całkowicie darmowe. Po jego stworzeniu, kliknięciu linka w mailu oraz ustawieniu hasła, zostaniemy przeniesieni do dashboardu Heroku.
Klikamy w link "Create new app" – na Heroku aplikacją nazywa się pojedynczy projekt – i przechodzimy do formularza tworzenia nowej aplikacji.
Najlepiej zacząć od zmiany regionu z United States na Europe – jeśli mamy taki wybór, lepiej mieć serwer na naszym kontynencie. ;)
Następnie możesz (ale nie musisz!) podać nazwę swojej aplikacji. Będzie to jednocześnie subdomena, czyli fragment adresu strony. Jeśli nie wpiszesz żadnej nazwy, Heroku wylosuje ją za Ciebie – będzie to coś w stylu shrouded-cove-991674, i wtedy adresem Twojej strony będzie:
http://shrouded-cove-991674.herokuapp.com/
Losowa nazwa to całkiem niezłe rozwiązanie dla projektów, które służą Ci do nauki programowania. Będziesz mieć możliwość zmiany nazwy i subdomeny później, a dzięki temu nie będziesz głowić się nad unikalną nazwą. Tak jest, nazwa aplikacji musi być unikatowa, dlatego na pewno będą zajęte takie nazwy jak blog, portfolio czy my-project.
Po zatwierdzeniu formularza zobaczysz stronę potwierdzającą założenie aplikacji. Na tej stronie interesuje Cię przede wszystkim guzik "Open app", który otworzy stronę Twojego projektu. Na razie jednak jeszcze niczego tam nie ma.
Instalacja Heroku CLI
Do obsługi Heroku będziemy potrzebować narzędzi, które pozwolą nam na łatwe zarządzanie aplikacjami. Zainstaluj Heroku CLI w wersji odpowiedniej dla Twojego systemu operacyjnego.
Po zainstalowaniu Heroku CLI, uruchom terminal w katalogu projektu i wykonaj w nim komendę heroku -v, aby sprawdzić, czy wszystko działa poprawnie.
Rozwiązywanie problemów na Windowsie
Możliwe, że otrzymasz komunikat o tym, że Twój system nie rozpoznaje powyższej komendy – w takiej sytuacji musisz ręcznie podać ścieżkę do Heroku CLI w zmiennej systemowej PATH.
W tym celu wejdź do panelu sterowania, a następnie do sekcji "System i zaawansowane ustawienia systemu". W oknie, które się otworzy, w zakładce "Zmienne środowiskowe", musimy dodać ścieżkę do Heroku. Jest to po prostu ścieżka do folderu bin w lokalizacji, w której masz zainstalowane Heroku – domyślnie jest to C:\Program Files\Heroku\bin.
Zaznaczamy opcję Path w kategorii "Zmienne systemowe", a następnie wybieramy "Edytuj" i dodajemy wspomnianą ścieżkę do folderu bin.
Teraz wszystko powinno działać, jak należy!
Wykonaj teraz komendę heroku login. Aplikacja poprosi Cię o Twoje dane logowania do Heroku, czyli email oraz ustawione przed chwilą hasło. Od tej chwili nie będzie już potrzeby ponownego podawania tych danych.
Publikacja projektu
Pozostał nam już tylko jeden krok do publikacji projektu! Składa się on z dwóch etapów... ;)
Po pierwsze, musimy skonfigurować adres zdalnego repozytorium na Heroku. Posłuży nam do tego komenda:
heroku git:remote -a shrouded-cove-991674
Zamiast shrouded-cove-991674 podaj nazwę swojej aplikacji.
Teraz możesz opublikować swoją stronę za pomocą komendy:
git push heroku master
Po wykonaniu tej komendy zobaczysz dość długi komunikat, którego kolejne linie będą wyświetlane przez parę minut. Jest to podgląd komunikatów terminala na serwerze Heroku, który wykonuje npm run build. Po zakończeniu budowania projektu zostanie wyświetlony link do Twojego projektu i zakończy się wykonanie tej komendy. Kilkanaście sekund później, Twój projekt powinien być opublikowany pod podanym adresem. Możesz szybko otworzyć stronę, uruchamiając w terminalu komendę heroku open.
W razie problemów może zdarzyć się, że domyślna konfiguracja aplikacji będzie wymagała małej korekty – uruchom wtedy komendę heroku ps:scale web=1. Jeśli strona nie zaczęła działać, wprowadź jakąś zmianę w kodzie, zapisz commit i ponownie wykonaj git push heroku master.
Dodatkowe informacje o Heroku CLI
Jeśli zechcesz opublikować w przyszłości inne projekty, możesz skorzystać ze znacznie szybszego sposobu tworzenia aplikacji.
Zamiast tworzyć aplikację za pomocą dashboardu Heroku, jak zrobiliśmy to przed chwilą, można to robić również za pomocą linii komend. W tym celu możesz użyć komendy:
heroku create
Opcjonalnie po spacji możesz też dodać nazwę aplikacji, którą chcesz stworzyć.
heroku create app_name
Ta komenda jednocześnie doda adres zdalnego repozytorium do konfiguracji Gita, dzięki czemu nie trzeba wywoływać komendy heroku git:remote -a app_name. W rezultacie, od razu po wykonaniu heroku create można uruchomić git push heroku master.
Zasypianie aplikacji
Darmowe konto na Heroku ma kilka limitów – większości z nich nie odczujemy w żaden sposób, poza jednym. Jest to usypianie aplikacji. Jeśli przez 30 minut strona nie będzie wyświetlona, serwer zawiesi działanie naszej aplikacji. Oznacza to, że kolejne otwarcie strony zajmie ok. 10-20 sekund.
W trakcie nauki nie stanowi to żadnego problemu, ale kiedy rozpoczniesz poszukiwanie pracy i umieścisz swoje projekty na Heroku, warto o tym pamiętać. Rekruter może nie mieć na tyle cierpliwości, aby doczekać się wyświetlenia strony.
Z tego względu warto wtedy rozważyć potencjalne rozwiązania tego problemu:
- rezygnacja z Heroku na rzecz innego hostingu obsługującego Node.js,
- wykupienie najtańszego pakietu w Heroku,
- publikacja plików projektu np. na GitHub Pages, a wyłącznie API na Heroku.
W ostatnim rozwiązaniu należałoby nieco zoptymalizować działanie strony – np. dodać komunikat o wczytywaniu danych na podstronach, które korzystają z API. Warto też zaoferować użytkownikowi więcej "atrakcji", zanim dojdzie do tych podstron – może to być np. dodatkowa podstrona, zawierająca galerię zdjęć.
W naszej ocenie warto jednak zastanowić się, ile czasu zajmie wdrożenie któregoś z tych rozwiązań. Jeśli będzie to dla Ciebie przyjemność – nie ma problemu! Jeżeli zaś miałoby to odbyć się kosztem czasu, który lepiej byłoby spożytkować na inne cele, rozważ, czy nie jest to warte opłacenia konta na Heroku.
Zadanie: deploy na Heroku
Jeśli wszystko poszło dobrze, Twoja strona jest już opublikowana – czyli zdeploy'owana – na serwerze Heroku. W takim wypadku to zadanie zajmie Ci tylko moment!
W katalogu projektu (tam, gdzie package.json) powinien znajdować się plik README.md. Jeśli go nie masz, stwórz go. Dodaj do tego pliku link do strony opublikowanej na Heroku. Następnie zapisz commit i wyślij go na zdalne repozytorium za pomocą git push.
Na stronie GitHuba znajdź swoje repozytorium tego projektu. Zawartość pliku README.md powinna wyświetlać się pod listą plików, a link powinien być klikalny.
To wszystko – Twoim zadaniem było opublikowanie strony oraz dodanie linka do pliku README.md. Gratulacje! :)
11.6. Rozwijamy projekt (dla ambitnych)
Jeśli czujesz, że to, co dotychczas zrobiliśmy to dla Ciebie zdecydowanie za mało, mamy w zanadrzu jeszcze kilka pomysłów, które możesz wprowadzić w życie.
Ten submoduł nie jest już obowiązkowy, ale naprawdę mocno zachęcamy Cię do spróbowania swoich sił. JS ma to do siebie, że dobrze przyswoić można go tylko poprzez praktykę.
Opcja 1: zmiana tematyki strony
Pierwszym pomysłem jest zmiana tematyki strony. Nie wystarczy jednak zmienić wyłącznie logo czy nazwy produktów! Strona ma być kompletnie nie do poznania!
W ramach tego zadania należy zmienić co najmniej:
- kolorystykę strony,
- logo i slogan w nagłówku,
- wygląd koszyka i zakładek kierujących do podstron,
- nazwy podstron,
- układ i zawartość strony głównej,
- produkty w menu (co najmniej 4 produkty inne niż dotychczasowe),
- ilustracje produktów (co najmniej 2 produkty mają mieć ilustracje warstwowe, zmieniające się w zależności od wybranych opcji, inne niż dotychczasowe),
- opcje produktów (co najmniej 1 produkt musi mieć parametry i opcje inne niż dotychczasowe),
- zakres dat i godzin możliwych rezerwacji miejsc,
- mapę restauracji,
- opcje rezerwacji (co najmniej 3 opcje inne niż dotychczasowe),
- wydarzenia cykliczne, blokujące dostępność rezerwacji, mają odbywać się z częstotliwością inną niż codzienna,
- dodać co najmniej jedną podstronę.
Nie ograniczamy w żaden sposób tematyki strony – może to być parking strzeżony, hotel, kafejka internetowa, kino, czy dowolna inna tematyka. Może to być nawet restauracja. Liczy się jedynie spełnienie powyższych wymagań.
Opcja 2: kolorowe tło suwaka wyboru godziny
W tej chwili tło suwaka jest szare, a dodatkowo część na lewo od znacznika jest zielona. Twoim zadaniem jest zmiana tła suwaka tak, aby:
- nie zmieniało się przy przesuwaniu suwaka,
- godziny, w których wszystkie stoliki są zajęte, miały czerwone tło suwaka,
- godziny, w których dostępny jest tylko jeden stolik, miały pomarańczowe tło suwaka,
- pozostałe godziny miały zielone tło suwaka.
Dzięki temu po wyborze daty od razu będzie widać, czy w danym dniu są jakieś wolne stoliki. Jeśli np. użytkownik zastanawia się, którego dnia wybrać się na kolację z gronem znajomych, będzie mu dużo łatwiej znaleźć dogodny termin.
W wykonaniu tego zadania może pomóc przypomnienie sobie aplikacji z książkami i funkcjonalności "kolorowania" paska ratingu. Pamiętaj również, że gradient może stworzyć tło z kilku równych kolorów.
Np.
body {
background: linear-gradient(to right,
red 20%, orange 20% 40%, yellow 40% 60%, green 60% 80%, blue 80%);
}
...da nam efekt:
Opcja 3: wykluczenie błędnych rezerwacji
Załóżmy, że stolik 2 jest zajęty od godziny 13:00. Obecnie użytkownik może zarezerwować ten stolik od godziny 12:00 nawet na 9 godzin, a więc dwie rezerwacje się pokryją. Podobnie, mimo że restauracja jest czynna do północy, można zarezerwować stolik od 23:00 również na 9 godzin.
Twoim zadaniem jest wprowadzenie funkcjonalności, która w momencie wyboru stolika ograniczy czas rezerwacji, który można wybrać w sekcji "HOURS". W obu podanych powyżej przykładach maksymalnym czasem będzie 1 godzina.
Ponadto chcielibyśmy, aby była możliwość wyboru długości rezerwacji z dokładnością do pół godziny. Domyślną wartością powinno nadal być 1, ale powinna być możliwość zmiany wartości na 0.5, 1.5, 2, 2.5 itd.
Obie części tego zadania będą wymagały zmian w klasie AmountWidget. Pierwsza z nich dodatkowo będzie wymagać zmian w klasie Booking, a druga – w kodzie szablonu podstrony Booking w pliku index.html.
Pamiętaj, że te zmiany mają wpływać wyłącznie na wybór długości rezerwacji – pozostałe instancje AmountWidget mają działać jak do tej pory.
Opcja 4: rezerwacje kilku stolików
Do tej pory zakładaliśmy, że wszystkie wydarzenia i rezerwacje dotyczą wyłącznie jednego stolika. Twoim zadaniem jest taka zmiana naszej aplikacji, aby obejmowały one dowolną liczbę stolików.
Pamiętaj, że ta zmiana musi dotyczyć zarówno danych w API oraz agregacji informacji o zajętości stolików, jak również wyboru stolików w procesie składania rezerwacji. W efekcie po złożeniu rezerwacji na kilka stolików, na mapie restauracji powinna wyświetlać się zajętość wszystkich stolików, których dotyczy złożona rezerwacja.
Powodzenia!
11.7. Podsumowanie
Na tym kończymy pracę nad tym projektem – przynajmniej na razie. Udało nam się stworzyć całkiem sporą aplikację, jak na początek nauki programowania. Po drodze stworzyliśmy też jeden trochę łatwiejszy projekt (lista książek) oraz wykonaliśmy kilkanaście mniejszych zadań. To naprawdę dużo, ale to wszystko to dopiero początek, ponieważ dobry programista nigdy nie przestaje się uczyć i rozwijać!
Książka warta przeczytania
Jednym z najlepszych dostępnych materiałów do pogłębiania wiedzy z zakresu JS-a jest książka Eloquent JavaScript, opublikowana bezpłatnie w internecie. W języku polskim dostępna jest wyłącznie pierwsza edycja tej książki, więc gorąco zachęcamy do lektury drugiego wydania w oryginale.
Co udało Ci się zrobić?
Skupmy się przez chwilę na tym, co dokładnie udało nam się zrobić. Spróbujmy obejrzeć stronę pizzerii oczami przeciętnego zjadacza internetu – zwykłego użytkownika, który nie tworzy stron, tylko je konsumuje.
Na pierwszy rzut oka widzimy skromną stronę, której grafika jest stonowana, aby przenieść uwagę na zdjęcia czy ilustracje produktów. Wybraliśmy minimalistyczne podejście, aby potencjalny klient nie gubił się w gąszczu linków – dzięki temu ma szybki dostęp do tego, co go interesuje.
Strona zamawiania jest na pierwszy rzut oka przejrzysta i nieskomplikowana – jednocześnie jednak zawiera rzadko spotykane funkcjonalności, takie jak możliwość zmiany domyślnych składników czy ilustracje odwzorowujące opcje produktu wybrane przez użytkownika. Dzięki temu będzie zapadać w pamięć i pozwalać na dostosowanie zamówienia do indywidualnych preferencji.
Przechodząc na stronę rezerwacji stolików, użytkownik napotka miłą niespodziankę. Spodziewałby się raczej numeru telefonu, pod którym można złożyć rezerwację – albo co najwyżej formularza kontaktowego. Nasza aplikacja pozwala jednak nie tylko na sprawdzenie dostępności stolików, ale również na wybranie konkretnego z nich. Dzięki temu potencjalny klient będzie mógł wybrać stolik z dala od wejścia, przy oknie, czy w sali dla palących – w zależności od tego, jak rozbudujemy mapę restauracji.
Takie rozwiązania można spotkać na stronach linii lotniczych czy sieci kin – a my pokazaliśmy, że można je z powodzeniem stosować również na stronie małej pizzerii, stworzonej w relatywnie krótkim czasie! Zresztą, teraz kiedy strona jest już opublikowana w internecie, możesz wysłać ją znajomym i zapytać, co sądzą o jej funkcjonalności. ;)
Zastosowane rozwiązania
Ten projekt był, oczywiście, pretekstem do nauki JS-a w podejściu obiektowym i nabrania większej wprawy w realizacji nieco większych zadań. Udało Ci się stworzyć ten projekt z naszą pomocą - dostarczyliśmy Ci kod i szablony HTML, style SCSS oraz pomocnicze funkcje. Jednak wszystkie te elementy poznaliśmy już wcześniej i potrafisz je już samodzielnie stworzyć.
Dzięki temu, w ostatnich modułach mogliśmy skupić się na nauce JS-a. Co więcej, ten projekt był dla nas okazją, aby pokazać Ci trochę bardziej rozbudowane podejście do stylów projektu.
Metodologia BEM
Zdecydowaliśmy się na przeznaczenie pliku style.scss wyłącznie na importowanie pozostałych plików .scss. Dzięki temu pełni on wyłącznie rolę organizacyjną, a wszystkie style są w mniejszych plikach, z których każdy ma konkretne przeznaczenie.
Co ważniejsze, zaprezentowaliśmy w praktyce jedną z popularnych metodologii – BEM. Jej nazwa jest skrótem od słów Block, Element i Modifier, które stanowią podstawę tej metodologii. To podejście pozwala na tworzenie bardzo dużych projektów, z zachowaniem jednolitej konwencji nazewnictwa klas elementów HTML. W wielkim skrócie, dzięki temu udaje się uniknąć bałaganu, nawet kiedy pracuje się nie na kilku, ale kilkuset plikach .scss.
Gorąco zachęcamy Cię do poczytania o niej nieco więcej – nie jest ona elementem tego kursu, ale zdecydowanie warto się z nią zapoznać. Jej zrozumienie pomoże Ci dalej rozwijać swoje zdolności rozumienia elementów stron jako komponentów, które łatwo rozwijać i modyfikować w skali całego projektu.
Szablony Handlebars
Już wcześniej poznaliśmy moc szablonów HTML, ale dopiero w tym projekcie zaczęliśmy wykorzystywać je na szerszą skalę. Pozwoliły nam zupełnie odseparować trzy obszary naszego kodu: treść, wygląd i funkcjonalność. Dzięki temu nasz kod JS jest wolny od fragmentów HTML-a czy treści wyświetlanych na stronie. Pozwala to na większą uniwersalność aplikacji i znacznie prostszy rozwój – szczególnie gdybyśmy chcieli przygotować inną wersję językową strony.
Konfiguracja skryptu
Idąc dalej w stronę ułatwienia rozwoju aplikacji, wszystkie dane konfiguracyjne zebraliśmy w pliku settings.js. Dzięki temu ewentualne zmiany w kodzie HTML nie będą wymagały godzin przeszukiwania JS. To bardzo ważne, aby wszystkie selektory i inne ustawienia były łatwo dostępne. Dzięki temu inne osoby pracujące nad projektem mogą bez problemu je modyfikować.
Napisaliśmy "inne osoby", bo mogą to być nie tylko inni developerzy, ale też np. designerzy, specjaliści SEO, czy nawet sam klient. Niektórzy z nich mają wystarczającą wiedzę, by np. samodzielnie zmieniać kod HTML i style, ale nie znają JS-a. Dzięki podejściu, które przyjęliśmy, mogą samodzielnie dostosowywać selektory, z których korzysta nasza aplikacja, zamiast z każdym drobiazgiem zgłaszać się do programisty – czyli do Ciebie.
Funkcje użytkowe
Nasza biblioteka utils nie jest obowiązkowym elementem projektu – równie dobrze wszystkie jej metody mogliśmy zaimplementować w poszczególnych klasach, w których je wykorzystujemy. Zdecydowaliśmy się na ich wydzielenie z dwóch względów. Pierwszym z nich jest dobra praktyka – są to głównie funkcje konwertujące dane w jakiś określony sposób. Nie są ściśle związane z żadną konkretną funkcjonalnością i mogą się przydać w wielu klasach.
Drugim powodem są walory edukacyjne – często te funkcje są dość żmudne do wytłumaczenia, a wiele z nich wprowadziłoby więcej chaosu w Twojej wiedzy, niż mogłoby Cię nauczyć.
Podejście obiektowe
Cała nasza aplikacja opiera się na klasach i ich instancjach. Pozwoliło nam to na sprawną obsługę wielu produktów, zarówno w menu produktów, jak i w koszyku. Mieliśmy też przykład widgetu do wyboru liczby sztuk, osób czy godzin – który wykorzystaliśmy wielokrotnie w różnych komponentach.
Przy okazji pozwoliło nam to również na lepszą organizację naszego kodu, który będzie łatwiejszy do zrozumienia i dalszej rozbudowy.
Stworzyliśmy też bazową klasę widgetu, której rozszerzenia dziedziczą jej metody. Dzięki temu nie musimy się już zastanawiać, jak odczytywać lub zmieniać wartość każdego poszczególnego widgetu.
AJAX i komunikacja z API
Poznaliśmy też sposoby komunikacji z serwerem z poziomu kodu JS. Pozwoliło nam to rozwinąć funkcjonalności wysyłki zamówień i rezerwacji do API, w którym zostały zapisane. Nie jest to jednak jedyne zastosowanie tej technologii – jest ona również bardzo przydatna przy optymalizacji działania stron internetowych, zawierających duże ilości informacji.
Przypomnij sobie nasz poprzedni projekt – bloga. Wspominaliśmy wtedy, że umieszczenie setek czy tysięcy postów w kodzie HTML nie byłoby dobrym rozwiązaniem. Zastosowanie AJAX-a w połączeniu z API pozwoliłoby na wczytywanie tylko tych informacji, które chcemy w danej chwili wyświetlić.
Kolejnym ciekawym zastosowaniem AJAX-a jest bieżąca aktualizacja informacji, szczególnie w przypadku usług takich jak komunikatory czy informacje giełdowe – ale nie tylko, bo nawet w przypadku rezerwacji stolików w restauracji informacja o zajętości miejsc mogłaby aktualizować się na bieżąco.
AJAX jest nieodzownym elementem dzisiejszego internetu i kluczową umiejętnością potrzebną na stanowisku Junior Web Developera.
Co dalej?
Na tym kończymy naukę waniliowego JS-a (vanilla JS), czyli czystego JavaScriptu, bez wykorzystania bibliotek czy frameworków. W dalszej części kursu nadal będziesz jednak wykorzystywać zdobyte umiejętności, ale wzbogacimy nasze aplikacje o bibliotekę React.js. Jest to jedno z najpopularniejszych rozwiązań na rynku, który znacznie rozszerzy nasze możliwości i pozwoli na szybsze tworzenie jeszcze bardziej zaawansowanych aplikacji.
Do zobaczenia w następnym module!